﻿using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using NopShip = Nop.Core.Domain.Shipping;

namespace Logistics.Data
{
    #if false
    public interface IRouteRepository
    {
        DbSet<Shipment> Shipments { get; set; }
        DbSet<Location> Locations { get; set; }
        DbSet<Address> Addresses { get; set; }
        DbSet<Contactor> Contactors { get; set; }
        DbSet<Driver> Drivers { get; set; }
        DbSet<Warehouse> Warehouses { get; set; }
        DbSet<Package> Packages { get; set; }
        DbSet<RoutePlan> RoutePlans { get; set; }

        DbContext DBContext { get; }

        string ImportShipment(
            Nop.Core.Domain.Common.Address deliveryAddress,
            Customer shipmentOwner,
            TimeWindow ownerPreferredTime,
            TimeWindow receiverPreferredTime,
            Nop.Core.Domain.Common.Address pickupAddress = null);
        Shipment GetShipmentById(int shipmentId);  // logistics id
        void PromptDriver(Customer driver,
                          String FirstName,
                          String LastName,
                          List<String> cityList);  // A customer is just a user. Think of user when setting a driver!
        void DemoteDriver(Customer courier);
        bool GetIdByExternalId(int externalId, out int id);
        //ICollection<Locality> GetCourierPreferredLocalitiesByExternalId(int externalId);
        RoutePlan GetPlanByDriverId(int driverId);
        IEnumerable<Driver> GetCurrentPlannedDrivers();
        IEnumerable<Driver> GetAvailableDrivers();
        IEnumerable<Package> GetPendingPlanPackages();
        void SetPackageAsPlanned(IEnumerable<Package> plannedPackages);
        void SavePlan(PlanType plan);
        int SaveChanges();
        string GetNewTrackingNumber();
    }


    public class RouteRepository : DbContext, IRouteRepository
    {
        public RouteRepository(string nameOrConnectionString)
            : base(nameOrConnectionString)
        {
        }

        public DbSet<Shipment> Shipments { get; set; }
        public DbSet<Location> Locations { get; set; }
        public DbSet<Address> Addresses { get; set; }
        public DbSet<Locality> Localities { get; set; }
        public DbSet<StateProvince> StateProvinces { get; set; }
        public DbSet<Country> Countries { get; set; }
        public DbSet<Contactor> Contactors { get; set; }
        public DbSet<Driver> Drivers { get; set; }
        public DbSet<Warehouse> Warehouses { get; set; }
        public DbSet<Package> Packages { get; set; }
        public DbSet<RoutePlan> RoutePlans { get; set; }

        public DbContext DBContext
        {
            get { return this; }
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            #region Address

            modelBuilder.Entity<Address>()
                .HasRequired(a => a.City)
                .WithMany(c => c.Addresses)
                .HasForeignKey(a => a.CityId);

            modelBuilder.Entity<Address>()
                .HasRequired(a => a.StateProvince)
                .WithMany(p => p.Addresses);

            modelBuilder.Entity<Address>()
                .HasRequired(a => a.Country)
                .WithMany(c => c.Addresses);

            #endregion

            #region Location

            modelBuilder.Entity<Location>()
                .HasRequired(l => l.Address)
                .WithOptional();

            modelBuilder.Entity<Location>()
                .HasOptional(l => l.RoutePlan)
                .WithMany(rp => rp.AssignedLocations);

            modelBuilder.Entity<Location>()
                .HasOptional(l => l.AssignedDriver)
                .WithMany()
                .WillCascadeOnDelete(false);

            modelBuilder.Entity<Location>()
                .HasRequired(l => l.Contactor)
                .WithMany(c => c.Locations)
                .WillCascadeOnDelete(false);

            #endregion

            #region Shipment

            modelBuilder.Entity<Shipment>()
                .HasRequired(o => o.PickUpLocation)
                .WithMany()
                .WillCascadeOnDelete(false);

            modelBuilder.Entity<Shipment>()
                .HasRequired(o => o.DeliveryLocation)
                .WithMany()
                .WillCascadeOnDelete(false);

            modelBuilder.Entity<Shipment>()
                .HasOptional(o => o.Package)
                .WithRequired(p => p.Shipment);

            modelBuilder.Entity<Shipment>()
                .HasOptional(o => o.Submitter)
                .WithMany()
                .WillCascadeOnDelete(false);

            #endregion

            #region Package

            modelBuilder.Entity<Package>()
                .HasOptional(p => p.Warehouse)
                .WithMany(w => w.Packages)
                .WillCascadeOnDelete(false);

            //modelBuilder.Entity<Package>()
            //    .HasRequired(p => p.PickUpLocation)
            //    //.WithRequired(l => l.Package)
            //    .WithMany()
            //    .WillCascadeOnDelete(false);

            //modelBuilder.Entity<Package>()
            //    .HasRequired(p => p.DeliveryLocation)
            //    //.WithRequired(l => l.Package)
            //    .WithMany()
            //    .WillCascadeOnDelete(false);

            #endregion

            #region Persons

            #region Driver

            //modelBuilder.Entity<Driver>()
            //    .HasRequired(u => u.UserInformation)
            //    .WithMany()
            //    .HasForeignKey(u => u.UserInformationId);

            modelBuilder.Entity<Driver>()
                .HasMany(d => d.PreferredLocalities)
                .WithMany()
                .Map(m => m.ToTable("Route_DriverPreferredLocality").MapLeftKey("DriverId").MapRightKey("LocalityId"));

            #endregion

            modelBuilder.Entity<Contactor>()
                .HasRequired(u => u.UserInformation)
                .WithMany();

            modelBuilder.Entity<Submitter>()
                .HasRequired(u => u.UserInformation)
                .WithMany();

            modelBuilder.Entity<Driver>()
                .HasRequired(u => u.UserInformation)
                .WithMany();

            #endregion

            #region RoutePlan

            modelBuilder.Entity<RoutePlan>()
                .HasMany(r => r.AssignedLocations)
                .WithOptional(l => l.RoutePlan)
                .WillCascadeOnDelete(false);

            modelBuilder.Entity<RoutePlan>()
                .HasRequired(r => r.Driver)
                .WithMany(d => d.RoutePlans);

            modelBuilder.Entity<RoutePlan>()
                .HasRequired(r => r.Warehouse)
                .WithMany();

            #endregion

            base.OnModelCreating(modelBuilder);
        }

        public string ImportShipment(
            Nop.Core.Domain.Common.Address deliveryAddress,
            Nop.Core.Domain.Customers.Customer shipmentOwner,
            TimeWindow ownerPreferredTime,
            TimeWindow receiverPreferredTime,
            Nop.Core.Domain.Common.Address pickupAddress = null)
        {
            // TODO: What is the appropriate store address in the future?
            // Currently user a shipmentOwner's billing address as the store address.
            // (I presume now that a store owner will have a special user account to operate a store).
            var ownerAddress = shipmentOwner.Addresses.First();
            var dAddress = pickupAddress == null ? ownerAddress : pickupAddress;

            if (deliveryAddress == null || dAddress == null || ownerAddress == null)
            {
                throw new ArgumentException("The shipment information is not valid.", "shipment|shipmentOwner");
            }
            return null;

            //var sp = ShipmentManager.WrapShipment(this,
            //    ShipmentManager.GenerateAddress(this, dAddress),
            //    ShipmentManager.GenerateAddress(this, deliveryAddress),
            //    new Contactor() // Pick up
            //    {
            //        UserInformation = new UserInformation()
            //        {
            //            ExternalId = shipmentOwner.Id,
            //            FirstName = ownerAddress.FirstName,
            //            LastName = ownerAddress.LastName,
            //            Account = shipmentOwner.Email,
            //        },
            //        PhoneNumber = ownerAddress.PhoneNumber,
            //        PreferredTimeWindow = TimeWindow.SevenAmToTenAm
            //    },
            //    new Contactor() // Delivery
            //    {
            //        UserInformation = new UserInformation()
            //        {
            //            ExternalId = deliveryAddress.Id,
            //            FirstName = deliveryAddress.FirstName,
            //            LastName = deliveryAddress.LastName,
            //            Account = deliveryAddress.Email,
            //        },
            //        PhoneNumber = deliveryAddress.PhoneNumber,
            //        PreferredTimeWindow = TimeWindow.TenAmToThreePm
            //    },
            //    new Submitter()
            //    {
            //        UserInformation = new UserInformation()
            //        {
            //            ExternalId = shipmentOwner.Id,
            //            FirstName = dAddress.FirstName,
            //            LastName = dAddress.LastName,
            //            Account = shipmentOwner.Email,
            //        }
            //    });

            //return sp.Id.ToString() + "-" + sp.TrackingNumber;
        }

        public bool GetIdByExternalId(int externalId, out int id)
        {
            // TODO: Index this id and add foreign key
            var users = new List<UserBase>();
            users.AddRange(Drivers);
            users.AddRange(Contactors);
            users.AddRange(Set<Submitter>());
            foreach (var u in users)
            {
                if (u.UserInformation.ExternalId.HasValue && u.UserInformation.ExternalId == externalId)
                {
                    id = u.Id;
                    return true;
                }
            }
            id = 0;
            return false;
        }

        public ICollection<Locality> GetCourierPreferredLocalitiesByExternalId(int externalId)
        {
            int id;
            if (GetIdByExternalId(externalId, out id))
            {
                Driver d = Drivers.Find(id);
                if (d != null)
                {
                    return d.PreferredLocalities;
                }
            }
            return null;
        }

        public void DemoteDriver(Customer courier)
        {
            int id;
            if (GetIdByExternalId(courier.Id, out id))
            {
                Driver d = Drivers.Find(id);
                if (d != null)
                {
                    Set<UserInformation>().Remove(d.UserInformation);
                    Drivers.Remove(d);
                    SaveChanges();
                }
            }
        }

        public Shipment GetShipmentById(int shipmentId)
        {
            return Shipments.Where(s => s.Id == shipmentId).Single();
        }

        public RoutePlan GetPlanByDriverId(int driverId)
        {
            return RoutePlans.Where(r => r.Status != RouteStatus.Completed && r.DriverId == driverId).FirstOrDefault();
        }

        public IEnumerable<Driver> GetAvailableDrivers()
        {
            var availableDrivers = new List<Driver>();
            foreach (var d in Drivers)
            {
                try
                {
                    var rp = RoutePlans.SingleOrDefault(
                        r => r.DriverId == d.Id && r.Status != RouteStatus.Completed);
                    if (null == rp)
                    {
                        availableDrivers.Add(d);
                    }
                }
                catch (InvalidOperationException e)
                {
                    throw new InvalidOperationException(
                        String.Format("A driver has been assigned more than one route. Driver: {0}:{1}", d.Id, d.DisplayName), e);
                }
            }
            return availableDrivers;
            // The following code throws NotSupportException
            //return Drivers.Where(d => (
            //    RoutePlans.SingleOrDefault(
            //    r => r.Status != RouteStatus.Completed && r.DriverId == d.Id) == null
            //    )).ToArray();
        }

        public IEnumerable<Driver> GetCurrentPlannedDrivers()
        {
            return RoutePlans.Where(rp => rp.Status == RouteStatus.NewlyPlanned).Select(rp => rp.Driver);
        }

        public IEnumerable<Package> GetPendingPlanPackages()
        {
            var w = Warehouses.First();
            var pkgs = Packages.Where(
                p => (
                    (p.Status == PackageStatus.ReadyForPickUp || p.Status == PackageStatus.InWarehouse)
                    && (p.Shipment.Status == ShipmentStatus.Pending)
                ));
            pkgs.ToList().ForEach(p => p.Warehouse = w);
            return pkgs;
        }

        public void SetPackageAsPlanned(IEnumerable<Package> plannedPackages)
        {
            var pkgs = new List<Package>(plannedPackages);

            var plans = from p in RoutePlans
                        where p.Status != RouteStatus.Completed
                        select p;

            foreach (var p in plans)
            {
                foreach (var l in p.AssignedLocations)
                {

                    if (pkgs.Find(pk => pk.Id == l.Shipment.Package.Id) != null)
                    {
                        p.Driver.Packages.Add(l.Shipment.Package);
                        l.Shipment.Status = ShipmentStatus.InProgress;
                        pkgs.Remove(l.Shipment.Package);
                    }
                    l.AssignedDriver = p.Driver;
                    TimeUtility.UpdateTimeStamp(l);
                }
            }

            SaveChanges();

            if (pkgs.Count > 0)
            {
                throw new InvalidOperationException("Some packages are not planned yet.");
            }
        }

        public void SavePlan(PlanType plan)
        {
            // TODO: Verify plan, resolve warehouse and so on
            Warehouse w = Warehouses.Single();

            foreach (var da in plan.DriverAssignments)
            {
                if (da.GoWaypoints == null && da.ReturningWaypoints == null && da.UnresolvedWaypoints == null)
                {
                    continue;
                }

                var wps = new List<Location>();
                if (da.GoWaypoints != null)
                {
                    wps.AddRange(da.GoWaypoints.OrderBy(wp => wp.Rank));
                }
                if (da.ReturningWaypoints != null)
                {
                    //wps.AddRange(da.ReturningWaypoints.Select(a => a.Address.FormattedAddress));
                    wps.AddRange(da.ReturningWaypoints.OrderBy(wp => wp.Rank));
                }

                //TODO: Log error here if unresolved.
                if (da.UnresolvedWaypoints != null)
                {
                    //wps.AddRange(da.UnresolvedWaypoints.Select(a => a.Address.FormattedAddress));
                    wps.AddRange(da.UnresolvedWaypoints.OrderBy(wp => wp.Rank));
                }

                var r = new RoutePlan() { DriverId = da.Driver.Id };
                wps.ForEach(wp => r.AssignedLocations.Add(wp));
                r.Warehouse = w;
                RoutePlans.Add(r);
            }
            SaveChanges();
        }

        public void PromptDriver(Customer driver,
                                 String FirstName,
                                 String LastName,
                                 List<String> cityList)
        {
            if (driver == null)
                throw new ArgumentNullException("driver");

            if (String.IsNullOrEmpty(FirstName))
                throw new ArgumentNullException("FirstName");

            int id;
            Driver dr;
            if (GetIdByExternalId(driver.Id, out id))
            {
                dr = Drivers.Find(id);
                dr.UserInformation.FirstName = FirstName;
                dr.UserInformation.LastName = LastName = LastName ?? "";
                dr.UserInformation.ExternalId = driver.Id;
            }
            else
            {
                dr = new Driver()
                {
                    UserInformation = new UserInformation()
                    {
                        FirstName = FirstName,
                        LastName = LastName ?? "",
                        Account = driver.Email,
                        ExternalId = driver.Id
                    }
                };
                Drivers.Add(dr);
            }

            foreach (var city in cityList)
            {
                dr.PreferredLocalities.Clear();
                dr.PreferredLocalities.Add(Localities.Single(l => l.Name == city));
            }

            SaveChanges();
        }


        public string GetNewTrackingNumber()
        {
            return Guid.NewGuid().ToString();
        }
    }

    public class RouteRepositoryInitializer<T> : DropCreateDatabaseAlways<T> where T : RouteRepository
    {
        protected override void Seed(T db)
        {
            Seed((IRouteRepository)db);
        }

        public static void CleanTable(IRouteRepository db, string tableName)
        {
            ((IObjectContextAdapter)db.DBContext).ObjectContext.ExecuteStoreCommand("TRUNCATE TABLE dbo.[" + tableName + "]");
        }

        public static void Seed(IRouteRepository db)
        {
            GenerateNamedEntities("Canada", db.Countries);

            GenerateNamedEntities("BC", db.StateProvinces);

            GenerateNamedEntities("Vancouver,Richmond,Burnaby,New Westminster", db.Localities);

            db.SaveChanges();

            if (db.Warehouses.Count() == 0)
            {
                db.Warehouses.Add(new Warehouse()
                {
                    Address = new Address()
                    {
                        Address1 = "7077 Beresford St.",
                        City = db.Localities.Single(cc => cc.Name == "Burnaby"),
                        ZipPostalCode = "V5E 4J5",
                        StateProvince = db.StateProvinces.Single(s => s.Name == "BC"),
                        Country = db.Countries.Single(cc => cc.Name == "Canada")
                    }
                });
            }

            GenerateDrivers(db);
            GenerateOrders(db);
            db.SaveChanges();
        }

        private static void GenerateNamedEntities<TEntity>(string data, DbSet<TEntity> set) where TEntity : EntityWithNameBase, new()
        {
            data.Split(',')
                .ToList()
                .ForEach(
                    s =>
                    {
                        if (set.SingleOrDefault(e => e.Name == s) == null)
                        {
                            set.Add(new TEntity() { Name = s });
                        }
                    });
        }

        private static void GenerateDrivers(IRouteRepository db)
        {
            if (db.Drivers.Count() > 0)
            {
                return;
            }
            var dr = db.Drivers.Add(new Driver()
            {
                UserInformation = new UserInformation()
                {
                    FirstName = "Tom",
                    LastName = "Driver",
                    Account = "Tom@Test.com"
                }
            });
            dr.PreferredLocalities.Add(db.Localities.Single(l => l.Name == "Burnaby"));
            dr.PreferredLocalities.Add(db.Localities.Single(l => l.Name == "New Westminster"));

            dr = db.Drivers.Add(new Driver()
            {
                UserInformation = new UserInformation()
                {
                    FirstName = "Jack",
                    LastName = "Driver",
                    Account = "Jack@Test.com"
                }
            });
            dr.PreferredLocalities.Add(db.Localities.Single(l => l.Name == "Richmond"));

            dr = db.Drivers.Add(new Driver()
            {
                UserInformation = new UserInformation()
                {
                    FirstName = "Jason",
                    LastName = "Driver",
                    Account = "Jason@Test.com"
                }
            });
            dr.PreferredLocalities.Add(db.Localities.Single(l => l.Name == "Vancouver"));
        }

        private static void GenerateOrders(IRouteRepository db)
        {
            //Contactor contactor = new Contactor()
            //{
            //    UserInformation = new UserInformation()
            //    {
            //        FirstName = "John",
            //        LastName = "Carter",
            //        Account = "John@Test.com"
            //    },
            //    PhoneNumber = "604-888-8888",
            //    PreferredTimeWindow = TimeWindow.SevenAmToTenAm
            //};
            //Contactor contactor = db.Contactors.First();
            if (db.Shipments.Count() > 0)
            {
                return;
            }

            //ShipmentManager.WrapShipment(db,
            //    new Address()
            //    {
            //        Address1 = "1497 West Broadway",
            //        City = db.Localities.Single(c => c.Name == "Vancouver"),
            //        ZipPostalCode = "V6H 1H7",
            //        StateProvince = db.StateProvinces.Single(s => s.Name == "BC"),
            //        Country = db.Countries.Single(c => c.Name == "Canada")
            //    },
            //    new Address()
            //    {
            //        Address1 = "5960 Ferguson Rd",
            //        City = db.Localities.Single(c => c.Name == "Richmond"),
            //        ZipPostalCode = "V7B 1M6",
            //        StateProvince = db.StateProvinces.Single(s => s.Name == "BC"),
            //        Country = db.Countries.Single(c => c.Name == "Canada")
            //    },
            //    new Contactor()
            //    {
            //        UserInformation = new UserInformation()
            //        {
            //            FirstName = "John",
            //            LastName = "Carter",
            //            Account = "John@Test.com"
            //        },
            //        PhoneNumber = "604-888-8888",
            //        PreferredTimeWindow = TimeWindow.SevenAmToTenAm
            //    },
            //    new Contactor()
            //    {
            //        UserInformation = new UserInformation()
            //        {
            //            FirstName = "John",
            //            LastName = "Carter",
            //            Account = "John@Test.com"
            //        },
            //        PhoneNumber = "604-888-8888",
            //        PreferredTimeWindow = TimeWindow.SevenAmToTenAm
            //    },
            //    new Submitter()
            //    {
            //        UserInformation = new UserInformation() { FirstName = "Tinny", LastName = "Owner", Account = "Tinny@Test.com" }
            //    }
            //);


            //ShipmentManager.WrapShipment(db,
            //    new Address()
            //    {
            //        Address1 = "505 West Broadway",
            //        City = db.Localities.Single(c => c.Name == "Vancouver"),
            //        ZipPostalCode = "V5Z 1E7",
            //        StateProvince = db.StateProvinces.Single(s => s.Name == "BC"),
            //        Country = db.Countries.Single(c => c.Name == "Canada")
            //    },
            //    new Address()
            //    {
            //        Address1 = "2818 Main Street",
            //        City = db.Localities.Single(c => c.Name == "Vancouver"),
            //        ZipPostalCode = "V5T 0C1",
            //        StateProvince = db.StateProvinces.Single(s => s.Name == "BC"),
            //        Country = db.Countries.Single(c => c.Name == "Canada")
            //    },
            //    new Contactor()
            //    {
            //        UserInformation = new UserInformation()
            //        {
            //            FirstName = "John",
            //            LastName = "Carter",
            //            Account = "John@Test.com"
            //        },
            //        PhoneNumber = "604-888-8888",
            //        PreferredTimeWindow = TimeWindow.SevenAmToTenAm
            //    },
            //    new Contactor()
            //    {
            //        UserInformation = new UserInformation()
            //        {
            //            FirstName = "John",
            //            LastName = "Carter",
            //            Account = "John@Test.com"
            //        },
            //        PhoneNumber = "604-888-8888",
            //        PreferredTimeWindow = TimeWindow.SevenAmToTenAm
            //    },
            //    new Submitter()
            //    {
            //        UserInformation = new UserInformation() { FirstName = "Tinny", LastName = "Owner", Account = "Tinny@Test.com" }
            //    }
            //);

            //ShipmentManager.WrapShipment(db,
            //    new Address()
            //    {
            //        Address1 = "4501 West 10th Avenue",
            //        City = db.Localities.Single(c => c.Name == "Vancouver"),
            //        ZipPostalCode = "V6R 2J2",
            //        StateProvince = db.StateProvinces.Single(s => s.Name == "BC"),
            //        Country = db.Countries.Single(c => c.Name == "Canada")
            //    },
            //    new Address()
            //    {
            //        Address1 = "6200 McKay Avenue",
            //        City = db.Localities.Single(c => c.Name == "Burnaby"),
            //        ZipPostalCode = "V5H 4M9",
            //        StateProvince = db.StateProvinces.Single(s => s.Name == "BC"),
            //        Country = db.Countries.Single(c => c.Name == "Canada")
            //    },
            //    new Contactor()
            //    {
            //        UserInformation = new UserInformation()
            //        {
            //            FirstName = "John",
            //            LastName = "Carter",
            //            Account = "John@Test.com"
            //        },
            //        PhoneNumber = "604-888-8888",
            //        PreferredTimeWindow = TimeWindow.SevenAmToTenAm
            //    },
            //    new Contactor()
            //    {
            //        UserInformation = new UserInformation()
            //        {
            //            FirstName = "John",
            //            LastName = "Carter",
            //            Account = "John@Test.com"
            //        },
            //        PhoneNumber = "604-888-8888",
            //        PreferredTimeWindow = TimeWindow.SevenAmToTenAm
            //    },
            //    new Submitter()
            //    {
            //        UserInformation = new UserInformation() { FirstName = "Tinny", LastName = "Owner", Account = "Tinny@Test.com" }
            //    }
            //);
        }
    }
    #endif
}
